package org.jpropelleralt.jview.ref.impl;

import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.text.ParseException;
import java.util.Map;

import javax.swing.JComponent;
import javax.swing.JSpinner;
import javax.swing.SpinnerModel;
import javax.swing.SpinnerNumberModel;
import javax.swing.JSpinner.DefaultEditor;

import org.jpropelleralt.box.Box;
import org.jpropelleralt.box.Boxes;
import org.jpropelleralt.change.Change;
import org.jpropelleralt.jview.JView;
import org.jpropelleralt.ref.Ref;
import org.jpropelleralt.sequence.Sequence;
import org.jpropelleralt.sequence.impl.Sequences;
import org.jpropelleralt.view.View;
import org.jpropelleralt.view.Views;
import org.jpropelleralt.view.update.UpdateManager;

/**
 * An editing {@link JView} for a {@link Ref} with value of type {@link Number}
 * @param <T>		The type of {@link Number} viewed
 */
public class NumberView<T extends Number> implements JView {

	private final Ref<T> value;
	private final Ref<Boolean> locked;
	
	private final UpdateManager updateManager;

	private final JSpinner spinner;

	private final AutoSpinnerModel model;
	
	private final Sequence<T> sequence;
	
	private final Class<T> valueClass;

	private NumberView(Class<T> valueClass, final Ref<T> value, Ref<Boolean> locked, Sequence<T> sequence, UpdateManager updateManager) {
		super();
		this.valueClass = valueClass;
		this.value = value;
		this.locked = locked;
		this.sequence = sequence;

		model = new AutoSpinnerModel();
		this.spinner = new LinkingJSpinner(this, model);
		applyFocusFix(spinner);
		
		this.updateManager = updateManager;
		updateManager.registerUpdatable(this);

		value.features().addView(this);
		if (locked != null) {
			locked.features().addView(this);
		}
		
		//Initial update
		update();
	}
	
	@Override
	public void update() {
		//Get our spinner model to tell the spinner value has changed
		model.fire();
		
		//Fix enabled state
		boolean shouldEnable = !Boxes.isTrue(locked);
		if (spinner.isEnabled() != shouldEnable) {
			spinner.setEnabled(shouldEnable);
		}
	}

	private class AutoSpinnerModel extends SpinnerNumberModel {
		
		private boolean firing = false;
		
		public void fire() {
			firing = true;
			fireStateChanged();
			firing = false;
		}

		@Override
		public T getNextValue() {
			return sequence.next(getValue());
		}

		@Override
		public T getPreviousValue() {
			return sequence.previous(getValue());
		}

		@Override
		public T getValue() {
			return value.get();
		}

		//Cast is safe, since we check against a Class instance that we know matches the parametric type
		@SuppressWarnings("unchecked")
		@Override
		public void setValue(Object spinnerValue) {
			//If we are just getting a change back as a result of firing a change
			//ourself, no need to respond
			if (!firing) {
				//Only set values that are different, AND are of value class
				if (!spinnerValue.equals(value.get()) && valueClass.isAssignableFrom(spinnerValue.getClass())) {
					value.set((T)spinnerValue);
				}
			}
		}
	}

	@Override
	public void changes(Map<Box, Change> changes) {
		updateManager.updateRequiredBy(this);
	}

	private void applyFocusFix(final JSpinner spinner) {
		final JComponent editor = spinner.getEditor();
		
		//If the editor is a default editor, we can work around
		//failure to commit when selecting a menu (and possibly other
		//problems) by monitoring the editor, and committing its edits when it loses
		//focus. This should happen automatically, and sometimes does by some
		//means I have not yet located, but fails when immediately selecting a menu 
		//after editing, etc.
		if (editor instanceof DefaultEditor) {
			final DefaultEditor dEditor = (DefaultEditor)editor;
			dEditor.getTextField().addFocusListener(new FocusListener() {
				
				//On focus lost, try to commit any pending edit.
				//If this fails, then revert editor to the actual
				//current value to avoid confusion
				public void focusLost(FocusEvent e) {
					try {
						spinner.commitEdit();
					} catch (ParseException e1) {
						update();
					}
				}
				
				//Do nothing on focus gained
				public void focusGained(FocusEvent e) {
				}
			});
		}
	}
	
	/**
	 * Get the {@link JSpinner} used for display/editing
	 * 
	 * @return		The {@link JSpinner}
	 */
	public JSpinner getComponent() {
		return spinner;
	}
	
	@Override
	public boolean selfNaming() {
		//Just displays value
		return false;
	}
	
	@Override
	public Format format() {
		return Format.SINGLE_LINE;
	}
	
	/**
	 * Create a new {@link NumberView}
	 * @param <T>			The type of {@link Number} viewed
	 * @param valueClass	The {@link Class} of {@link Number} viewed
	 * @param value			{@link Ref} containing the viewed value
	 * @param locked		While this is true, view will not permit editing (control disabled) 
	 * @param sequence		Sequence used for the up/down buttons on the spinner
	 * @param updateManager	{@link UpdateManager} to use
	 * @return				A new {@link NumberView}r
	 */
	public static <T extends Number> NumberView<T> create(Class<T> valueClass, final Ref<T> value, Ref<Boolean> locked, Sequence<T> sequence, UpdateManager updateManager) {
		return new NumberView<T>(valueClass, value, locked, sequence, updateManager);
	}

	/**
	 * Create a new {@link NumberView}
	 * @param <T>			The type of {@link Number} viewed
	 * @param valueClass	The {@link Class} of {@link Number} viewed
	 * @param value			{@link Ref} containing the viewed value
	 * @param locked		While this is true, view will not permit editing (control disabled) 
	 * @param sequence		Sequence used for the up/down buttons on the spinner
	 * @return				A new {@link NumberView}r
	 */
	public static <T extends Number> NumberView<T> create(Class<T> valueClass, final Ref<T> value, Ref<Boolean> locked, Sequence<T> sequence) {
		return new NumberView<T>(valueClass, value, locked, sequence, Views.updateManager());
	}

	/**
	 * Create a new {@link NumberView}
	 * @param <T>			The type of {@link Number} viewed
	 * @param valueClass	The {@link Class} of {@link Number} viewed
	 * @param value			{@link Ref} containing the viewed value
	 * @param sequence		Sequence used for the up/down buttons on the spinner
	 * @return				A new {@link NumberView}r
	 */
	public static <T extends Number> NumberView<T> create(Class<T> valueClass, final Ref<T> value, Sequence<T> sequence) {
		return new NumberView<T>(valueClass, value, null, sequence, Views.updateManager());
	}
	
	//AUTOCODE
	
	
// AutoCode for Integer

	/**
	 * Create a new {@link NumberView} for {@link Integer} instances
	 * @param value			{@link Ref} containing the viewed value
	 * @param locked		While this is true, view will not permit editing (control disabled) 
	 * @param step			Step size used for the up/down buttons on the spinner
	 * @param updateManager	{@link UpdateManager} to use
	 * @return				A new {@link NumberView}
	 */
	public static NumberView<Integer> createInteger(final Ref<Integer> value, Ref<Boolean> locked, Integer step, UpdateManager updateManager) {
		return new NumberView<Integer>(Integer.class, value, locked, Sequences.stepSequence(step), updateManager);
	}

	/**
	 * Create a new {@link NumberView} for {@link Integer} instances
	 * @param value			{@link Ref} containing the viewed value
	 * @param locked		While this is true, view will not permit editing (control disabled) 
	 * @param step			Step size used for the up/down buttons on the spinner
	 * @return				A new {@link NumberView}
	 */
	public static NumberView<Integer> createInteger(final Ref<Integer> value, Ref<Boolean> locked, Integer step) {
		return new NumberView<Integer>(Integer.class, value, locked, Sequences.stepSequence(step), Views.updateManager());
	}

	/**
	 * Create a new {@link NumberView} for {@link Integer} instances
	 * @param value			{@link Ref} containing the viewed value
	 * @param step			Step size used for the up/down buttons on the spinner
	 * @return				A new {@link NumberView}
	 */
	public static NumberView<Integer> createInteger(final Ref<Integer> value, Integer step) {
		return new NumberView<Integer>(Integer.class, value, null, Sequences.stepSequence(step), Views.updateManager());
	}

	/**
	 * Create a new {@link NumberView} for {@link Integer} instances
	 * @param value			{@link Ref} containing the viewed value
	 * @param locked		While this is true, view will not permit editing (control disabled) 
	 * @param step			Step size used for the up/down buttons on the spinner
	 * @return				A new {@link NumberView}
	 */
	public static NumberView<Integer> createInteger(final Ref<Integer> value) {
		return new NumberView<Integer>(Integer.class, value, null, Sequences.stepSequence(1), Views.updateManager());
	}


// End of AutoCode for Integer


// AutoCode for Long

	/**
	 * Create a new {@link NumberView} for {@link Long} instances
	 * @param value			{@link Ref} containing the viewed value
	 * @param locked		While this is true, view will not permit editing (control disabled) 
	 * @param step			Step size used for the up/down buttons on the spinner
	 * @param updateManager	{@link UpdateManager} to use
	 * @return				A new {@link NumberView}
	 */
	public static NumberView<Long> createLong(final Ref<Long> value, Ref<Boolean> locked, Long step, UpdateManager updateManager) {
		return new NumberView<Long>(Long.class, value, locked, Sequences.stepSequence(step), updateManager);
	}

	/**
	 * Create a new {@link NumberView} for {@link Long} instances
	 * @param value			{@link Ref} containing the viewed value
	 * @param locked		While this is true, view will not permit editing (control disabled) 
	 * @param step			Step size used for the up/down buttons on the spinner
	 * @return				A new {@link NumberView}
	 */
	public static NumberView<Long> createLong(final Ref<Long> value, Ref<Boolean> locked, Long step) {
		return new NumberView<Long>(Long.class, value, locked, Sequences.stepSequence(step), Views.updateManager());
	}

	/**
	 * Create a new {@link NumberView} for {@link Long} instances
	 * @param value			{@link Ref} containing the viewed value
	 * @param step			Step size used for the up/down buttons on the spinner
	 * @return				A new {@link NumberView}
	 */
	public static NumberView<Long> createLong(final Ref<Long> value, Long step) {
		return new NumberView<Long>(Long.class, value, null, Sequences.stepSequence(step), Views.updateManager());
	}

	/**
	 * Create a new {@link NumberView} for {@link Long} instances
	 * @param value			{@link Ref} containing the viewed value
	 * @param locked		While this is true, view will not permit editing (control disabled) 
	 * @param step			Step size used for the up/down buttons on the spinner
	 * @return				A new {@link NumberView}
	 */
	public static NumberView<Long> createLong(final Ref<Long> value) {
		return new NumberView<Long>(Long.class, value, null, Sequences.stepSequence(1l), Views.updateManager());
	}


// End of AutoCode for Long


// AutoCode for Float

	/**
	 * Create a new {@link NumberView} for {@link Float} instances
	 * @param value			{@link Ref} containing the viewed value
	 * @param locked		While this is true, view will not permit editing (control disabled) 
	 * @param step			Step size used for the up/down buttons on the spinner
	 * @param updateManager	{@link UpdateManager} to use
	 * @return				A new {@link NumberView}
	 */
	public static NumberView<Float> createFloat(final Ref<Float> value, Ref<Boolean> locked, Float step, UpdateManager updateManager) {
		return new NumberView<Float>(Float.class, value, locked, Sequences.stepSequence(step), updateManager);
	}

	/**
	 * Create a new {@link NumberView} for {@link Float} instances
	 * @param value			{@link Ref} containing the viewed value
	 * @param locked		While this is true, view will not permit editing (control disabled) 
	 * @param step			Step size used for the up/down buttons on the spinner
	 * @return				A new {@link NumberView}
	 */
	public static NumberView<Float> createFloat(final Ref<Float> value, Ref<Boolean> locked, Float step) {
		return new NumberView<Float>(Float.class, value, locked, Sequences.stepSequence(step), Views.updateManager());
	}

	/**
	 * Create a new {@link NumberView} for {@link Float} instances
	 * @param value			{@link Ref} containing the viewed value
	 * @param step			Step size used for the up/down buttons on the spinner
	 * @return				A new {@link NumberView}
	 */
	public static NumberView<Float> createFloat(final Ref<Float> value, Float step) {
		return new NumberView<Float>(Float.class, value, null, Sequences.stepSequence(step), Views.updateManager());
	}

	/**
	 * Create a new {@link NumberView} for {@link Float} instances
	 * @param value			{@link Ref} containing the viewed value
	 * @param locked		While this is true, view will not permit editing (control disabled) 
	 * @param step			Step size used for the up/down buttons on the spinner
	 * @return				A new {@link NumberView}
	 */
	public static NumberView<Float> createFloat(final Ref<Float> value) {
		return new NumberView<Float>(Float.class, value, null, Sequences.stepSequence(1f), Views.updateManager());
	}


// End of AutoCode for Float


// AutoCode for Double

	/**
	 * Create a new {@link NumberView} for {@link Double} instances
	 * @param value			{@link Ref} containing the viewed value
	 * @param locked		While this is true, view will not permit editing (control disabled) 
	 * @param step			Step size used for the up/down buttons on the spinner
	 * @param updateManager	{@link UpdateManager} to use
	 * @return				A new {@link NumberView}
	 */
	public static NumberView<Double> createDouble(final Ref<Double> value, Ref<Boolean> locked, Double step, UpdateManager updateManager) {
		return new NumberView<Double>(Double.class, value, locked, Sequences.stepSequence(step), updateManager);
	}

	/**
	 * Create a new {@link NumberView} for {@link Double} instances
	 * @param value			{@link Ref} containing the viewed value
	 * @param locked		While this is true, view will not permit editing (control disabled) 
	 * @param step			Step size used for the up/down buttons on the spinner
	 * @return				A new {@link NumberView}
	 */
	public static NumberView<Double> createDouble(final Ref<Double> value, Ref<Boolean> locked, Double step) {
		return new NumberView<Double>(Double.class, value, locked, Sequences.stepSequence(step), Views.updateManager());
	}

	/**
	 * Create a new {@link NumberView} for {@link Double} instances
	 * @param value			{@link Ref} containing the viewed value
	 * @param step			Step size used for the up/down buttons on the spinner
	 * @return				A new {@link NumberView}
	 */
	public static NumberView<Double> createDouble(final Ref<Double> value, Double step) {
		return new NumberView<Double>(Double.class, value, null, Sequences.stepSequence(step), Views.updateManager());
	}

	/**
	 * Create a new {@link NumberView} for {@link Double} instances
	 * @param value			{@link Ref} containing the viewed value
	 * @param locked		While this is true, view will not permit editing (control disabled) 
	 * @param step			Step size used for the up/down buttons on the spinner
	 * @return				A new {@link NumberView}
	 */
	public static NumberView<Double> createDouble(final Ref<Double> value) {
		return new NumberView<Double>(Double.class, value, null, Sequences.stepSequence(1d), Views.updateManager());
	}


// End of AutoCode for Double

	/**
	 * This is the same as the parent {@link JComponent}, 
	 * the only difference is that it remembers its {@link JView}, 
	 * so that as long as the {@link JComponent} is in
	 * use, the {@link JView} survives as well. Otherwise, the 
	 * {@link JView} can be GCed while the {@link JComponent} is still 
	 * in use, stopping the {@link JComponent} being updated.
	 * Otherwise, it is very easy for the {@link JView} to be collected,
	 * since a {@link View} is NOT kept alive by the data it is
	 * watching, and is generally not of much interest after its
	 * component has been added to a UI. 
	 */
	public static class LinkingJSpinner extends JSpinner {
		private final JView owner;
		private LinkingJSpinner(JView owner, SpinnerModel spinnerModel) {
			super(spinnerModel);
			this.owner = owner;
		}
		/**
		 * Get the owner {@link JView}
		 * @return	The owner of this component
		 */
		public JView owner() {
			return owner;
		}
	}
	
}
